/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.android.receiver;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

import org.json.JSONException;
import org.json.JSONObject;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import com.hercules.android.receiver.commands.XRECommandHandler;
import com.hercules.android.receiver.commands.XRECommandHandlerException;
import com.hercules.android.receiver.commands.XREConnectCommandHandler;

/**
 * <p>
 * XRE connection manager that is responsible for handling communication trough TCP.
 * </p>
 * 
 * @author TCSASSEMBER
 * @version 1.0
 */
public class ConnectionManager {
    /**
     * Array of bytes that are expected in the first message sent to the application. These data represent protocol
     * header as defined in section 2.6 of XRE Protocol Specification.
     */
    private static final byte[] PROTOCOL_HEADER = { 0x58, 0x52, 0x45, 0x0D, 0x0A };

    /**
     * Represents the <code>Socket</code> used to communication trough TCP to XRE server.
     */
    private Socket socket;

    /**
     * Represents the <code>Receiver</code> instance.
     */
    private Receiver receiver;

    /**
     * Represents the <code>Thread</code> used to communicate with XRE server.
     */
    private Thread thread;

    /**
     * A flag indicates whether the connection is closed.
     */
    private boolean closed = true;

    /**
     * Start a new thread to communicate with the XRE server.
     */
    public void startSocketThread() {
        if (thread != null || closed) {
            return;
        }
        final XRECommandHandler commandHandler = receiver.getCommandHandler();

        thread = new Thread() {
            /**
             * Call the main activity to show an error dialog.
             * 
             * @param message
             *            the error message to show
             */
            private void showErrorMessage(String message) {
                Bundle bundle = new Bundle();
                bundle.putString(Config.ERROR_MESSAGE_KEY, message);
                Handler handler = receiver.getApplicationData().getShowDialogHandler();
                Message msg = handler.obtainMessage();
                msg.setData(bundle);
                handler.sendMessage(msg);
            }

            /**
             * Read a XRE message from socket. Refer Section 2.6 in XRE specification documentation for details.
             * 
             * @return the XRE message
             * @throws ConnectionManagerException
             *             if any error occurs
             */
            private String getMessage() throws ConnectionManagerException {
                try {
                    DataInputStream is = new DataInputStream(socket.getInputStream());
                    // read the message length
                    int length = is.readInt();
                    // read the message
                    byte[] bytes = read(socket, length);
                    return new String(bytes);
                } catch (IOException e) {
                    throw new ConnectionManagerException("Error occurs when reading from socket", e);
                }
            }

            /**
             * The main function of this thread. If will read command messages from socket and handle the messages.
             */
            public void run() {
                while (!closed) {
                    try {
                        String message = getMessage();
                        Log.v(Config.APPLICATION_NAME, "Receiver message:" + message);
                        JSONObject jsonObject = new JSONObject(message);
                        commandHandler.onCommandMessage(jsonObject);
                    } catch (ConnectionManagerException e) {
                        if (!closed) {
                            Log.v(Config.APPLICATION_NAME, "Error occurs when getting message from socket.", e);
                            showErrorMessage("Error occurs when getting message from socket:" + e.getMessage());
                        }
                        return;
                    } catch (JSONException e) {
                        Log.v(Config.APPLICATION_NAME, "Can't parse message to JSON object.", e);
                        showErrorMessage("Can't parse message to JSON object:" + e.getMessage());
                        return;
                    } catch (XRECommandHandlerException e) {
                        Log.v(Config.APPLICATION_NAME, "Error occurs when handling the message.", e);
                        showErrorMessage("Error occurs when handling the message:" + e.getMessage());
                        return;
                    }
                }
            }
        };
        thread.start();
    }

    /**
     * Create a new <code>ConnectionManager</code>. It will connect to the XRE server and start a new thread to
     * communicate with the server.
     * 
     * @param receiver
     *            the <code>Receiver</code> instance
     * @param host
     *            the host of XRE server
     * @param port
     *            the port of XRE server
     * @throws IllegalArgumentException
     *             if receiver is null
     * @throws ConnectionManagerException
     *             if can't connect to XRE server
     */
    public ConnectionManager(final Receiver receiver, String host, int port) throws ConnectionManagerException {
        ReceiverUtil.checkNull("receiver", receiver);

        this.receiver = receiver;
        socket = new Socket();

        try {
            Log.v(Config.APPLICATION_NAME, "Connect to " + host + ":" + port);
            socket.connect(new InetSocketAddress(host, port), Config.CONNECT_TIMEOUT);
            closed = false;
            Log.v(Config.APPLICATION_NAME, "Sending the protocol header");
            socket.getOutputStream().write(PROTOCOL_HEADER);
        } catch (IOException e) {
            throw new ConnectionManagerException("Can't connect to " + host + ":" + port, e);
        } catch (IllegalArgumentException e) {
            throw new ConnectionManagerException("Server host or port is invalid.", e);
        }
    }

    /**
     * Send an event to XRE server.
     * 
     * @param event
     *            the event to send
     * @throws IllegalArgumentException
     *             if event is null or empty
     * @throws ConnectionManagerException
     *             if any error occurs
     */
    public void sendEvent(String event) throws ConnectionManagerException {
        ReceiverUtil.checkString("event", event);

        Log.v(Config.APPLICATION_NAME, "Sending event:" + event);
        byte[] bytes = event.getBytes();
        try {
            DataOutputStream os = new DataOutputStream(socket.getOutputStream());
            os.writeInt(bytes.length);
            os.write(bytes);
        } catch (IOException e) {
            throw new ConnectionManagerException("Error occurs when writing data to socket.", e);
        }
    }

    /**
     * Send a onConnect event to XRE server.
     * 
     * @throws ConnectionManagerException
     *             if any error occurs
     */
    public void connect() throws ConnectionManagerException {
        sendEvent(ReceiverUtil.getConnectionEvent(receiver));
        new XREConnectCommandHandler(receiver).processCommand(null);
    }

    /**
     * Read number of bytes from socket.
     * 
     * @param socket
     *            the <code>Socket</code> to read data from
     * @param count
     *            the number of bytes need to read
     * @return the bytes read from socket
     * @throws ConnectionManagerException
     *             if any error occurs
     */
    private static byte[] read(Socket socket, int count) throws ConnectionManagerException {
        byte[] bytes = new byte[count];
        int index = 0;
        int left = count;
        try {
            while (left > 0) {
                int readCount = socket.getInputStream().read(bytes, index, left);
                if (readCount == -1) {
                    throw new ConnectionManagerException("Connection was closed.");
                }
                index += readCount;
                left -= readCount;
            }
        } catch (IOException e) {
            throw new ConnectionManagerException("Error occurs when reading from socket", e);
        }
        return bytes;
    }

    /**
     * Close the connection.
     */
    public void close() {
        if (closed) {
            return;
        }
        closed = true;
        thread = null;
        if (socket != null) {
            try {
                socket.close();
                socket = null;
            } catch (IOException e) {
                // ignore
            }
        }
    }
}
